How to create a block to upload files to the media library (JS) in WordPress

Contents

Overview

This tutorial shows, in complete detail, how to create a Gutenberg block that lets editors upload files to the WordPress Media Library using JavaScript. It covers two common approaches: using the built-in MediaUpload component (media modal and uploader), and programmatically uploading files via the data store using wp.data.dispatch(core).uploadMedia. It also explains how to register the block, provide editor UI, save attributes, render on the front end, and handle permissions, multiple files, allowed MIME types, progress and error handling.

Prerequisites

  • WordPress 5.8 (Gutenberg integrated or plugin-enabled)
  • Node.js (v14 ) and npm (or yarn)
  • @wordpress/scripts for bundling (recommended) or your preferred build setup
  • Editor user with upload_files capability to test uploading

Recommended project structure

A simple block plugin can use this layout:

  • my-file-uploader-block/
    • package.json
    • src/
      • index.js
      • edit.js
      • save.js
      • styles/editor.css
      • styles/style.css
    • build/ (generated)
    • block.json
    • my-file-uploader-block.php

Quick setup with @wordpress/create-block or @wordpress/scripts

Use create-block scaffolder or set up your own with @wordpress/scripts. Example package.json scripts:

{
  name: my-file-uploader-block,
  version: 1.0.0,
  scripts: {
    build: wp-scripts build,
    start: wp-scripts start
  },
  devDependencies: {
    @wordpress/scripts: ^25.0.0
  }
}

Define block metadata (block.json)

Use block.json so WordPress can register assets automatically when you use register_block_type or the block scaffolds. Keep attributes that store attachment IDs, URLs or metadata.

{
  apiVersion: 2,
  name: my-plugin/file-uploader,
  title: File Uploader,
  category: media,
  icon: upload,
  description: Upload files to the media library and insert references.,
  supports: {
    html: false
  },
  editorScript: file-uploader-block-editor,
  editorStyle: file-uploader-block-editor-style,
  style: file-uploader-block-style,
  attributes: {
    files: {
      type: array,
      default: []
    },
    allowedTypes: {
      type: array,
      default: []
    },
    multiple: {
      type: boolean,
      default: true
    }
  }
}

Register block PHP (plugin bootstrap)

Register your block and enqueue built assets (if not auto-handled). Also check capability for rendering or server-side rendering.


High-level design choices

  1. Use MediaUpload component: easiest integration with the media modal and the uploader (provides select/upload UI). Good for standard workflows.
  2. Programmatic upload: use wp.data.dispatch(core).uploadMedia to upload files you obtain from an input[type=file] or drag-and-drop handler. This gives full control over upload UI (progress, custom metadata, grouping) and is required if you want to bypass the modal.
  3. Store attachment IDs in block attributes: store lightweight references (IDs) and render full data either client-side (query store) or server-side (render_callback) for stable front-end output.

Editor: using MediaUpload (recommended for standard use)

The MediaUpload component opens the media modal, supports uploading, selecting multiple or single, and returns attachment objects via onSelect. Use MediaUploadCheck to ensure the user can upload.

Edit component (MediaUpload example)

import { registerBlockType } from @wordpress/blocks
import { useSelect } from @wordpress/data
import {
  MediaUpload,
  MediaUploadCheck,
  BlockControls,
  InspectorControls
} from @wordpress/block-editor
import { Button, PanelBody, ToggleControl } from @wordpress/components
import { Fragment } from @wordpress/element

registerBlockType( my-plugin/file-uploader, {
  edit: ( props ) => {
    const { attributes, setAttributes } = props
    const { files = [], multiple = true, allowedTypes = [] } = attributes

    // Fetch full attachment objects from the store if you only store IDs
    const attachments = useSelect( ( select ) => {
      const { getMedia } = select( core )
      return files.map( ( f ) => {
        return typeof f === number ? getMedia( f ) : f
      } )
    }, [ files ] )

    const onSelect = ( selection ) => {
      // selection will be an object (single) or array (multiple)
      const next = Array.isArray( selection ) ? selection : [ selection ]
      // Store IDs to keep data compact
      setAttributes( {
        files: next.map( ( a ) => a.id ),
      } )
    }

    const removeFile = ( id ) => {
      setAttributes( { files: files.filter( ( fid ) => fid !== id ) } )
    }

    return (
      
        
          {/ optional controls /}
        
        
          
             setAttributes( { multiple: val } ) }
            />
          
        

        
( ) } />
{ attachments attachments.map( ( a ) => a ? (
{ a.title.rendered a.filename a.name }
) : null ) }
) }, save: () => { // Well render on the front end using saved attributes (IDs) or allow the Editors save to store JSON. return null }, } )

Notes about this approach

  • MediaUpload handles both selecting existing files and uploading new ones via the modal UI.
  • Store attachment IDs (integers) in attributes for compact storage and stability across environments.
  • Use useSelect to map IDs to full attachment objects for preview inside the editor.
  • allowedTypes accepts an array of MIME types (e.g., [application/pdf, image/]) or leave undefined to allow all types the site permits.
  • MediaUploadCheck ensures the UI only shows if the current user can upload files.

Editor: programmatic upload using wp.data.dispatch(core).uploadMedia

This technique is useful if you want a custom upload UI or drag-and-drop without the media modal. The core data store provides an uploadMedia action that will POST the file to the REST API, create an attachment and return the media object.

Key points

  • uploadMedia takes an object: { file, title, alt, post } — file is a File/Blob.
  • It returns a Promise that resolves to the created attachment object.
  • The REST nonce must be present scripts enqueued in WP admin have it set automatically if dependencies include wp-api-fetch and are registered properly.

Example edit component using an input[type=file] uploader

import { useState } from @wordpress/element
import { Button } from @wordpress/components
import { useDispatch, useSelect } from @wordpress/data

export default function Edit( { attributes, setAttributes } ) {
  const { files = [], multiple = true } = attributes
  const { uploadMedia } = useDispatch( core )
  const [ uploading, setUploading ] = useState( false )
  const attachments = useSelect( ( select ) => {
    const { getMedia } = select( core )
    return files.map( ( id ) => getMedia( id ) )
  }, [ files ] )

  const onFileInput = async ( event ) => {
    const inputFiles = Array.from( event.target.files )
    if ( inputFiles.length === 0 ) return
    setUploading( true )
    try {
      const uploaded = []
      for ( const file of inputFiles ) {
        // You can pass metadata like title or alt here
        const media = await uploadMedia( {
          file,
          title: file.name,
        } )
        uploaded.push( media.id )
      }
      const nextFiles = multiple ? [ ...files, ...uploaded ] : uploaded.slice( 0, 1 )
      setAttributes( { files: nextFiles } )
    } catch ( err ) {
      // handle error (display to user)
      console.error( Upload failed, err )
    } finally {
      setUploading( false )
    }
  }

  return (
    
{ uploading

Uploading...

}
{ attachments attachments.map( ( a ) => a ? : null ) }
) }

Progress reporting

The data store uploadMedia doesnt provide progress events directly. For advanced progress reporting, you can:

  • Use wp.apiFetch with custom fetch options and FormData, adding an onprogress handler by using XMLHttpRequest instead of fetch.
  • Use the REST route /wp/v2/media and a custom XHR upload to expose progress.

Saving and front-end rendering

Decide whether to save full markup in the post content (save component) or save minimal data (IDs) and render server-side with a render_callback. Storing IDs is more stable server-side rendering can fetch the latest attachment URLs and metadata.

Client-side save (serialized HTML)

// save.js - example when storing links directly in content
export default function save( { attributes } ) {
  const { files = [] } = attributes
  // If you stored objects, you can render anchors. But keep content portable.
  return (
    
{ files.map( ( f, index ) => { // If f is an object with url/title OR if you stored a minimal object, handle accordingly. const url = typeof f === object ? f.url : const title = typeof f === object ? ( f.title f.name ) : return { title url } } ) }
) }

Server-side render example (recommended if saving IDs)

Use register_block_type with a render_callback that outputs attachment URLs based on stored IDs. This ensures the front-end always shows the correct URL and metadata regardless of editor JS state.


    foreach ( attributes[files] as id ) {
        id = absint( id )
        if ( ! id ) {
            continue
        }
        url = wp_get_attachment_url( id )
        title = get_the_title( id )
        mime = get_post_mime_type( id )
        html .= sprintf(
            
%2s (%3s)
, esc_url( url ), esc_html( title ?: basename( url ) ), esc_html( mime ) ) } html .=
return html } // When registering the block server-side call register_block_type with render_callback pointing to mfu_render_callback. // Example: register_block_type( __DIR__ . /block.json, array( render_callback => mfu_render_callback ) ) ?>

Handling allowed MIME types, sanitization and security

Multiple files vs single file

Use a boolean attribute (multiple) to toggle the UI behavior:

Error handling and edge cases

Advanced: add metadata on upload

You can attach additional metadata on upload by calling uploadMedia with title, alt_text, caption, or by making a separate REST call to update the attachment after upload. Example sequence:

  1. uploadMedia({ file }) -> returns media object with ID
  2. wp.apiFetch( { path: /wp/v2/media/{id}, method: POST, data: { title, alt_text } } )
import apiFetch from @wordpress/api-fetch

async function uploadAndSetMeta( file, meta = {} ) {
  const { uploadMedia } = wp.data.dispatch( core )
  const uploaded = await uploadMedia( { file } )
  if ( meta  uploaded?.id ) {
    await apiFetch( {
      path: /wp/v2/media/{ uploaded.id },
      method: POST,
      data: meta,
    } )
  }
  return uploaded
}

Bundling and dependencies

If you use @wordpress/scripts, your editor script will have dependencies resolved automatically in the build asset manifest (index.asset.php). When registering the script on the server, include these dependencies so WordPress loads the correct blocks packages (wp-blocks, wp-element, wp-data, wp-editor, wp-components, wp-api-fetch, etc.).

Complete minimal example (index.js entry)

Heres an entry file that registers the block and references the edit/save modules. Use this with your bundler.

import ./styles/editor.css
import ./styles/style.css
import Edit from ./edit
import save from ./save
import { registerBlockType } from @wordpress/blocks

registerBlockType( my-plugin/file-uploader, {
  edit: Edit,
  save,
} )

Tips, best practices and testing

Complete checklist before shipping

  1. Block metadata (block.json) correct and attributes declared.
  2. Editor script registered and dependencies provided.
  3. Media upload UI handles permissions (MediaUploadCheck) and edge cases.
  4. Attributes sanitized in server render callback or save.
  5. Front-end rendering tested for missing attachments and invalid IDs.
  6. Build process configured (npm run build) and all assets enqueued.

Conclusion

This tutorial covered everything needed to create a Gutenberg block that uploads files to the media library: using MediaUpload for the built-in workflow, programmatic uploads via the core data store, storing and sanitizing attachment IDs, server-side rendering best practices, error handling, progress considerations, metadata updates, and build/registration steps. Use the MediaUpload component for the simplest, integrated UX use the uploadMedia action for custom uploader UI or advanced flows.

Reference links



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *